package com.querie.beans;


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * This class is used to calculate min hash value matrix for the users. For computing similarity
 * between users, their min hash signatures are compared.
 * 
 * @author smittal
 *
 */
public class MinHash {
	
    private static MinHash INSTANCE = null;
    private final int m_numHashFunctions = 100;
    HashFunctions hashFunctions = null;
    private Long universeSize = 999999L;
    private DatabaseAccess databaseAccess = null;
    private List<String> aLogic = new ArrayList<String>();
    
    public static MinHash getInstance() throws Exception {
    	if (INSTANCE == null) {
            INSTANCE = new MinHash();
    	}
    	
    	return INSTANCE;
    }
    

    /**
     * Constructor
     * 
     * @throws Exception
     */
    private MinHash() throws Exception
    {
    	databaseAccess = DatabaseAccess.getInstance();
    	hashFunctions = databaseAccess.getHashFunctionsFromDB(m_numHashFunctions);
    	
    	if (hashFunctions == null) {
	        Random r = new Random(11);
	        hashFunctions = new HashFunctions(m_numHashFunctions);
	        
	        for (int i = 0; i < m_numHashFunctions; i++)
	        {
	            long a = Math.abs(r.nextLong());
	            long b = Math.abs(r.nextLong());
	            long c = Math.abs(r.nextLong());
	            
	            hashFunctions.getA_s()[i] = a;
	            hashFunctions.getB_s()[i] = b;
	            hashFunctions.getC_s()[i] = c;
	        }
	        
	        databaseAccess.saveHashFunctionsToDB(m_numHashFunctions, hashFunctions);	        
    	}
    }
    
    /**
     * External API function. Call if we want to see only similar users.
     * Here set id is the session id for the new user and newSet corresponds to the tuple ids he has touched.
     * 
     * @param setId
     * @param newSet
     * @return
     * @throws Exception
     */
    public Set<Integer> findSimilarSets(int setId, List<Long> newSet) throws Exception {
    	Set<Integer> setIds = new HashSet<Integer>();
    	setIds.add(setId);

    	Map<Integer, List<Long>> sessionValues = new HashMap<Integer, List<Long>>();
    	sessionValues.put(setId, newSet);
    	
    	List<Long> minHashValues = this.ComputeMinHashForNewSet(sessionValues);
    	appendListToALogic("Min hash vales computed for the user are: ", minHashValues);
    	Set<Integer> sessionIds = this.similarity(setId, minHashValues);
//    	appendListToALogic("Similar users found are: ", sessionIds);
    	return sessionIds;
    	
    }

    /**
     * External API function. This is the function that actually interacts with the front-end and returns
     * all the similar queries calculated. Here set id is the session id for the new user and newSet
     * corresponds to the tuple ids he has touched.
     * 
     * @param setId
     * @param newSet
     * @return
     * @throws Exception
     */
    public List<String> findSimilarQueries(int setId, String userQuery, List<Long> newSet,
    		List<Long> evaluationTupleIds) throws Exception {
    	List<String> queryStrings = new ArrayList<String>();
    	queryStrings.add(userQuery);
    	aLogic.clear();
    	databaseAccess.saveTupleIdsToMySQL(setId, userQuery, newSet);
    	
    	Set<Integer> similarSetIds = this.findSimilarSets(setId, newSet);
    	
    	Set<Long> predictedVector = databaseAccess.computePredictedVector(setId, similarSetIds);
    	if (predictedVector != null) {
	    	aLogic.add("Size of the predicted vector calculated  for current user session is: " + 
	    			predictedVector.size());
    	}
    	else {
    		aLogic.add("Size of the predicted vector calculated  for current user session is: " + 
	    			0);
    	}
    
    	List<String> similarQueries = databaseAccess.findSimilarQueries(predictedVector, setId, similarSetIds,
    	evaluationTupleIds);
  
    	return similarQueries;
      }
    
    
    public void endUserSession(int sessionId) throws Exception {
    	Set<Integer> setIds = new HashSet<Integer>();
    	setIds.add(sessionId);
    	
    	//LSH.clusterSetsByLSH(setIds, m_numHashFunctions);
    }
    

    /**
     * External API function. This uses LSH approach to find similar users and finds the most similar user id.
     * Then it recommends all the queries of the most similar user as recommendations. It is not used
     * right now since results were bad with LSH technique. However, it can be used and experimented with
     * in future.
     * 
     * @param setId
     * @param newSet
     * @return
     * @throws Exception
     */
    public List<String> findRecommendedQueriesUsingLSH(int setId, List<Long> newSet) throws Exception {
    	Set<Integer> setIds = new HashSet<Integer>();
    	setIds.add(setId);

    	Map<Integer, List<Long>> sessionValues = new HashMap<Integer, List<Long>>();
    	sessionValues.put(setId, newSet);
    	List<Long> minHashValues = this.ComputeMinHashForNewSet(sessionValues);

    	LSH.clusterSetsByLSH(setIds, m_numHashFunctions);

    	Set<Integer> similarSets = this.similarity(setId, minHashValues);

    	Map<Integer, Double> similarityValues = new HashMap<Integer, Double>();
    	if (similarSets != null) {
    		for (Integer similarSet : similarSets) {
    			List<Long> minHashForSimilarSet = databaseAccess.getMinHashValues(similarSet);
    			double similarityValue = this.ComputeSimilarityFromSignatures(minHashValues,
    					minHashForSimilarSet, m_numHashFunctions);
    			similarityValues.put(similarSet, similarityValue);
    		}
    	}

    	// Find most similar set id.
    	int maxSimilarSet = 0;
    	double maxSimilarityValue = -99999999;
    	Iterator<Integer> it = similarityValues.keySet().iterator();

    	while (it.hasNext()) {
    		int setIdValue = it.next();
    		double value = similarityValues.get(setIdValue);
    		if (value > maxSimilarityValue) {
    			maxSimilarSet = setIdValue;
    		}
    	}

    	return databaseAccess.getQueriesForSessionId(maxSimilarSet);
    }

    /**
     * This is the function that actually computes min hash values for the new user.
     * 
     * @param sessionValues
     * @return
     * @throws Exception
     */
    public List<Long> ComputeMinHashForNewSet(Map<Integer, List<Long>> sessionValues) throws Exception
    {
    	Iterator<Integer> it = sessionValues.keySet().iterator();
    	List<Long> hashValueRow = null;
    	while (it.hasNext()) {
    		Integer setId = it.next();
    		
    		// If user's second query is coming in, then he already has some min hash values in the DB.
    		// Recalculate them with the new query.
    		hashValueRow = databaseAccess.getMinHashValues(setId);
    		boolean insertNew = false;
    		
    		if (hashValueRow == null) {
    			hashValueRow = new ArrayList<Long>();
	    		// Initialize hash value row for the user to some max value.
	            for (int j = 0; j < getM_numHashFunctions(); j++)
	            {
	            	hashValueRow.add(Long.MAX_VALUE);
	            }
	            insertNew = true;
    		}
    		
    		List<Long> setValues = sessionValues.get(setId);

    		for (int i = 0; i < m_numHashFunctions; i++) {
	    		if (setValues != null) {
	    			for (Long value : setValues) {
	    				Long hashValue = QHash(value, hashFunctions.getA_s()[i], hashFunctions.getB_s()[i], hashFunctions.getC_s()[i],
	    						universeSize);
	    				if (hashValue < hashValueRow.get(i)) {
	    					hashValueRow.set(i, hashValue);
	    				}
	    			}
	    		}
    		}
    		
    		databaseAccess.saveMinHashValues(setId, hashValueRow, insertNew);
    	}
    	
    	return hashValueRow;
    }
    
    /**
     * This function is used to compute hash values for all the user tuples touched.
     * @param x
     * @param a
     * @param b
     * @param c
     * @param bound
     * @return hash value
     */
    private long QHash(long x, long a, long b, long c, long bound)
    {
        long hashValue = (a * (x >> 4) + b * x + c) & 131071;
  
        return Math.abs(hashValue);
    }

    /**
     * This function is used to compute similarity from signatures. This function is being used by LSH similarity
     * calculation.
     * 
     * @param set1MinHash
     * @param set2MinHash
     * @param numHashFunctions
     * @return similarity value
     */
    private double ComputeSimilarityFromSignatures(List<Long> set1MinHash, List<Long> set2MinHash,
    		int numHashFunctions)
    {
        int identicalMinHashes = 0;
        for (int i = 0; i < numHashFunctions; i++)
        {
            if (set1MinHash.get(i).equals(set2MinHash.get(i)))
            {
                identicalMinHashes++;
            }
        }
        return (1.0 * identicalMinHashes) / numHashFunctions;
    }
    
    /**
     * This function is just computing similarity from min hash values and not taking LSH buckets to account.
     * 
     * @param minHashOfQuery
     * @return
     * @throws Exception
     */
 	 private Set<Integer> similarity(Integer setId, List<Long> minHashOfQuery) throws Exception
 	 {
        DatabaseAccess databaseAccess = DatabaseAccess.getInstance();
        List<Integer> sessionIds = databaseAccess.readSessionIds();
        // Remove the set in consideration.
        sessionIds.remove(setId);
        Set<Integer> similarSets = new HashSet<Integer>();
        Map<Integer, Integer> similarityMap = new HashMap<Integer, Integer>();
        
        for (Integer sessionId : sessionIds) {
     	   List<Long> values = databaseAccess.getMinHashValues(sessionId);
     	   int similarity = 0;
     	   
     	   for (int i = 0; i < minHashOfQuery.size(); i++) {
     		   if (values.get(i).equals(minHashOfQuery.get(i))) {
     			   similarity++;
     		   }	    		   
     	   }
     	   
     	   similarityMap.put(sessionId, similarity);	   
        }
        Collection<Integer> values = similarityMap.values();
        Set<Integer> similarities = new HashSet<Integer>();
        similarities.addAll(values);
        
        List<Integer> maxSimilarities = new ArrayList<Integer>();
        int count = similarities.size() > 2 ? 2 : similarities.size();
        
 		for (int i = 0; i < count; i++) {
 			Integer max = maxSimilarity(similarities);
 			similarities.remove(max);
 			
 			if (max != 0) {
 				maxSimilarities.add(max);
 			}
 		}
 		List<Integer> similarSessionIds = getSessionIdsWithMaxSimilarities(similarityMap,
 				maxSimilarities);
 		
 		similarSets.addAll(similarSessionIds);
 	
       // System.out.println("Max similarity = " + maxSimilarity);
        
       // similarSets.add(maxSimilarSessionId);
 		System.out.println("Similarity values: " + maxSimilarities);
 		//appendListToALogic("Similarities values found for users are :", maxSimilarities);
        System.out.println("Max similar session ids = " + similarSets);
        
        return similarSets;
 	}
 	
 	/**
 	 * This function gets the query ids with maximum similarity values.
 	 * 
 	 * @param similarityValueMap
 	 * @param maxSimilarities
 	 * @return List of query ids.
 	 */
 	private List<Integer> getSessionIdsWithMaxSimilarities(Map<Integer, Integer> similarityValueMap,
 			List<Integer> maxSimilarities) {
 		List<Integer> similarSessionIds = new ArrayList<Integer>();
 		
 		label1: for (Integer maxSimilarity: maxSimilarities) {
 			Iterator<Integer> it = similarityValueMap.keySet().iterator();
 			while(it.hasNext()) {
 				Integer key = it.next();
 				if (similarityValueMap.get(key).equals(maxSimilarity)) {
 					similarSessionIds.add(key);
 					
 					System.out.println("Similar session id: " + key + " Similarity value: " + maxSimilarity);
 					aLogic.add("Similar session id: " + key + " Similarity value: " + maxSimilarity); 					
 				}
 				if (similarSessionIds.size() >= 2) {
 					break label1;
 				}
 			}
 		}
 		
 		return similarSessionIds;
 	}

 	/**
 	 * This is a function to find the maximum similarity value out of a given set of values.
 	 * 
 	 * @param similarities
 	 * @return Maximum similarity value.
 	 */
 	private int maxSimilarity(Set<Integer> similarities) {
 		int max = 0;
 		
 		Iterator<Integer> it = similarities.iterator();
 		
 		while(it.hasNext()) {
 			int next = it.next();
 			if (max < next) {
 				max = next;
 			}
 		}
 		
 		return max;
 	}
 	
 	private void appendListToALogic(String header, Collection<?> list) {
 		Iterator<?> it = list.iterator();		
 		StringBuilder stringValue = new StringBuilder();
		
 		while (it.hasNext()) {
			stringValue.append(String.valueOf(it.next()));
			stringValue.append(",");
		}
 		if (stringValue.length()>0)
 			stringValue.deleteCharAt(stringValue.length() - 1);
 		
		aLogic.add(header);
		aLogic.add(stringValue.toString());
 	}

	public int getM_numHashFunctions() {
		return m_numHashFunctions;
	}
	public List<String> getALogic() {
		return aLogic;
	}	
}
